// -*- mode:java; encoding:utf-8 -*-
// vim:set fileencoding=utf-8:
// @homepage@

package example;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.text.ParseException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.text.DefaultCaret;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.JTextComponent;
import javax.swing.text.MaskFormatter;

public final class MainPanel extends JPanel {
  private MainPanel() {
    super();
    add(new TimePickerSingleField().createMainPanel());
    add(new TimePickerSplitFieldDemo().createPickerPanel());
    setBorder(BorderFactory.createEmptyBorder(20, 2, 20, 2));
    setPreferredSize(new Dimension(320, 240));
  }

  public static void main(String[] args) {
    EventQueue.invokeLater(MainPanel::createAndShowGui);
  }

  private static void createAndShowGui() {
    try {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    } catch (UnsupportedLookAndFeelException ignored) {
      Toolkit.getDefaultToolkit().beep();
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
      Logger.getGlobal().severe(ex::getMessage);
      return;
    }
    JFrame frame = new JFrame("@title@");
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    frame.getContentPane().add(new MainPanel());
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }
}

class TimePickerSplitFieldDemo {
  public JPanel createPickerPanel() {
    JFormattedTextField fieldHour = makeNumberField(12, 1, 0, 23);
    JFormattedTextField fieldMinute = makeNumberField(30, 1, 0, 59);

    JPanel pnlUp = new JPanel(new GridLayout(1, 2));
    pnlUp.add(makeCenteredBox(makeArrowButton(fieldHour, 1, 0, 23)));
    pnlUp.add(makeCenteredBox(makeArrowButton(fieldMinute, 1, 0, 59)));

    JPanel pnlDown = new JPanel(new GridLayout(1, 2));
    pnlDown.add(makeCenteredBox(makeArrowButton(fieldHour, -1, 0, 23)));
    pnlDown.add(makeCenteredBox(makeArrowButton(fieldMinute, -1, 0, 59)));

    JPanel panel = new JPanel(new BorderLayout(5, 5));
    panel.setOpaque(false);
    panel.add(pnlUp, BorderLayout.NORTH);
    panel.add(makeTimeFieldPanel(fieldHour, fieldMinute));
    panel.add(pnlDown, BorderLayout.SOUTH);
    return panel;
  }

  public static JButton makeArrowButton(JTextField field, int delta, int min, int max) {
    String txt = delta > 0 ? "⏶" : "⏷";
    JButton button = new JButton(txt);
    button.setFocusable(false);
    AutoRepeatHandler handler = new AutoRepeatHandler(field, delta, min, max);
    button.addActionListener(handler);
    button.addMouseListener(handler);
    return button;
  }

  private static Box makeCenteredBox(JButton button) {
    Box box = Box.createHorizontalBox();
    box.add(Box.createHorizontalGlue());
    box.add(button);
    box.add(Box.createHorizontalGlue());
    return box;
  }

  private static JPanel makeTimeFieldPanel(JTextField hour, JTextField minute) {
    JPanel panel = new RoundPanel(8);
    panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
    panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    panel.setOpaque(false);
    panel.setBackground(new Color(0xDE_DE_DE));
    panel.add(Box.createHorizontalGlue());
    panel.add(hour);
    JLabel colon = new JLabel(":");
    colon.setFont(colon.getFont().deriveFont(42f));
    colon.setBorder(BorderFactory.createEmptyBorder(0, 5, 10, 5));
    panel.add(colon);
    panel.add(minute);
    panel.add(Box.createHorizontalGlue());
    return panel;
  }

  public static JFormattedTextField makeNumberField(int value, int step, int min, int max) {
    JFormattedTextField field = new RoundFormattedTextField(value, step, min, max);
    try {
      MaskFormatter mask = new MaskFormatter("##");
      mask.setPlaceholderCharacter('0');
      field.setFormatterFactory(new DefaultFormatterFactory(mask));
    } catch (ParseException ex) {
      UIManager.getLookAndFeel().provideErrorFeedback(field);
    }
    field.setFont(field.getFont().deriveFont(42f));
    field.setHorizontalAlignment(JTextField.CENTER);
    field.setColumns(2);
    return field;
  }
}

class RoundPanel extends JPanel {
  private final int radius;

  protected RoundPanel(int radius) {
    super();
    this.radius = radius;
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(getBackground());
    int w = getWidth();
    int h = getHeight();
    g2.fill(new RoundRectangle2D.Double(0, 0, w, h, radius, radius));
    g2.setColor(getBackground().darker());
    g2.draw(new RoundRectangle2D.Double(0, 0, w - 1, h - 1, radius, radius));
    g2.dispose();
    super.paintComponent(g);
  }
}

class RoundFormattedTextField extends JFormattedTextField {
  private transient FocusListener listener;

  protected RoundFormattedTextField(int value, int step, int min, int max) {
    super(String.format("%02d", value));
    // setText(String.format("%02d", value));
    addMouseWheelListener(e -> {
      int delta = e.getWheelRotation() < 0 ? 1 : -1;
      Component c = e.getComponent();
      if (c instanceof JTextComponent) {
        AutoRepeatHandler.adjust((JTextComponent) c, delta * step, min, max);
      }
    });
  }

  @Override public void updateUI() {
    removeFocusListener(listener);
    super.updateUI();
    // setFont(getFont().deriveFont(42f));
    // setHorizontalAlignment(CENTER);
    // setColumns(2);
    setFocusable(true);
    setOpaque(false);
    setBackground(new Color(0xCE_CE_CE));
    setSelectionColor(new Color(0x0, true));
    setSelectedTextColor(getForeground());
    // setMargin(new Insets(4, 4, 4, 4));
    setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
    setCaret(new DefaultCaret() {
      @Override public boolean isVisible() {
        return false;
      }
    });
    setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
    listener = new FocusListener() {
      @Override public void focusGained(FocusEvent e) {
        Component c = e.getComponent();
        c.setForeground(UIManager.getColor("TextField.foreground"));
      }

      @Override public void focusLost(FocusEvent e) {
        e.getComponent().setForeground(Color.DARK_GRAY);
      }
    };
    addFocusListener(listener);
  }

  @Override protected void paintComponent(Graphics g) {
    if (hasFocus()) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      g2.setColor(getBackground());
      g2.fill(new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), 8, 8));
      g2.setColor(getBackground().darker());
      g2.draw(new RoundRectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1, 8, 8));
      g2.dispose();
    }
    super.paintComponent(g);
  }
}

class AutoRepeatHandler extends MouseAdapter implements ActionListener {
  private final Timer autoRepeatTimer;
  private final JTextComponent view;
  private final int delta;
  private final int min;
  private final int max;
  private JButton arrowButton;

  protected AutoRepeatHandler(JTextComponent view, int delta, int min, int max) {
    super();
    this.view = view;
    this.delta = delta;
    this.min = min;
    this.max = max;
    autoRepeatTimer = new Timer(60, this);
    autoRepeatTimer.setInitialDelay(300);
  }

  public static void adjust(JTextComponent field, int delta, int min, int max) {
    field.requestFocusInWindow();
    int range = max - min + 1;
    int value = Integer.parseInt(field.getText());
    value = (value - min + delta) % range;
    if (value < 0) {
      value += range;
    }
    value += min;
    field.setText(String.format("%02d", value));
  }

  @Override public void actionPerformed(ActionEvent e) {
    Object o = e.getSource();
    if (o instanceof Timer) {
      boolean released = Objects.nonNull(arrowButton) && !arrowButton.getModel().isPressed();
      if (released && autoRepeatTimer.isRunning()) {
        autoRepeatTimer.stop();
      }
    } else if (o instanceof JButton) {
      arrowButton = (JButton) o;
    }
    adjust(view, delta, min, max);
  }

  @Override public void mousePressed(MouseEvent e) {
    if (SwingUtilities.isLeftMouseButton(e) && e.getComponent().isEnabled()) {
      autoRepeatTimer.start();
    }
  }

  @Override public void mouseReleased(MouseEvent e) {
    autoRepeatTimer.stop();
  }

  @Override public void mouseExited(MouseEvent e) {
    if (autoRepeatTimer.isRunning()) {
      autoRepeatTimer.stop();
    }
  }
}

class TimePickerSingleField {
  private JFormattedTextField timeField;
  private LocalTime currentTime = LocalTime.of(12, 30);
  private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");

  public Component createMainPanel() {
    try {
      MaskFormatter mask = new MaskFormatter("##:##");
      mask.setPlaceholderCharacter('0');
      timeField = new JFormattedTextField(mask);
    } catch (ParseException ex) {
      timeField = new JFormattedTextField();
    }

    timeField.setFont(new Font("Monospaced", Font.BOLD, 42));
    timeField.setHorizontalAlignment(JTextField.CENTER);
    timeField.setEditable(false);
    timeField.setFocusable(true);
    updateDisplay();

    timeField.addMouseWheelListener(e -> {
      boolean isUp = e.getWheelRotation() < 0;
      // Java 9: boolean isHourSide = timeField.viewToModel2D(e.getPoint()) <= 2;
      boolean isHourSide = timeField.viewToModel(e.getPoint()) <= 2;
      adjustTime(isHourSide, isUp);
    });
    return timeField;
  }

  private void adjustTime(boolean isHour, boolean isUp) {
    if (isHour) {
      currentTime = isUp ? currentTime.plusHours(1) : currentTime.minusHours(1);
    } else {
      currentTime = isUp ? currentTime.plusMinutes(1) : currentTime.minusMinutes(1);
    }
    updateDisplay();
  }

  private void updateDisplay() {
    timeField.setText(currentTime.format(timeFormatter));
  }
}
